相信大家多多少少都有撰寫過Unit Test的程式,當然在軟體開發的過程中,可能因為時程或其他外在因素而導致無法持之以恆。
但套句Ruddy老師的話,『要相信雲端的程式,還是Local端的程式?都不是,應該要相信測試過的程式』。
一個程式怎麼樣才算完成可以交付,怎麼證明這個程式沒有問題,應該就要有一份測試程式來證明,這些程式在這些test case裡面,程式是沒有問題的。
在Martin Fowler的Refactoring Improving the Design of Existing Code 第一章裡提到,The First Step in Refactoring:
Whenever I do refactoring, the first step is always the same. I need to build a solid set of tests for that section of code. The tests are essential because even though I follow refactorings structured to avoid most of the opportunities for introducing bugs, I'm still human and still make mistakes.
重構是維持系統可維護性幾近於無可避免的動作,想要讓系統更乾淨、更有效率、更好維護,第一件事仍然是撰寫好測試程式,因為原本程式是可以正常運作,為了效率更好、更容易維護而導致程式結果錯誤,這是不被允許的。
撰寫可自動化的測試程式,是一個貫穿整個軟體開發過程的活動,從還沒開始撰寫實際程式,到未來維護、改版時都仍須倚重於這些測試程式。
其實在前面重構系列的文章中,有一篇就提到了Stub的用法與原由,請參考:[如何提升系統品質-Day7]測試-單元測試, Just Do It!!,這一篇則是由單元測試為切入點,來說明單元測試的一些重要資訊。
[如何提升系統品質]系列文章連結
單元測試的意義
單元測試的意義,是希望每一個測試的method,都有相當簡單明確的意義,就是要證明某一項功能在某一個case底下,程式是如預期一般運作的。
為什麼我們需要單元測試
因為整合測試有這幾個缺點,
1.整合測試無法快速的定位出錯誤點。
整合測試為黑箱測試,當測試失敗時,只知道其中的功能有錯,而無法快速準確的定位是哪一個『單位』錯了。就像下圖一樣,中間有好多的路徑都可能會出錯。
2.整合測試花費的時間太久,需要的測試環境太複雜。
正因為整合測試要跑得功能太多,環境也可能太複雜(需要外界的檔案、DataBase、服務等等...),所以需要花很多時間。對工程師來說,即使寫好測試程式,按一下測試要等2~5分鐘,是讓人相當氣餒的。 久而久之,越跑越久,久到乾脆不跑了。
3.整合測試涵蓋率不足。
即使我們使用了測試涵蓋率的工具,知道了哪些程式被整合測試測過,哪些沒被測過。我們卻不容易從整合測試的切入點,來補足沒有被涵蓋到的點。(雷公在雲上,要打到特定的人也是很有可能打歪的...)
以上的問題,都可以透過單元測試來解決。(前提是單元測試要寫對)
[註]基本上一個單元測試的執行時間如果超過0.1秒,就是一個很緩慢的單元測試程式。
何謂整合測試
簡單的說,就是與外部服務有相依的測試,我們稱之為整合測試。
何謂外部服務,例如以下幾種:
1.需連到資料庫。
2.需使用到網路。
3.需進行檔案存取(IO)。
4.需對測試環境進行特別的動作(例如需要先編輯設定檔,才能執行測試)。
單元測試應具備的特性
簡單用一個縮寫來表示:FIRST (參考自代碼整潔之道)
Fast:快速。
Independent:獨立。
Repeatable:可重複。
Self-Validating:可反應驗證結果。單元測試不論成功或失敗,都應該要從測試的reporting直接瞭解其意義或失敗原因。
Timely:及時。單元測試應該恰好在使其通過的production code之前撰寫。
如何寫單元測試,而非整合測試
斬斷與外界服務的直接相依性,怎麼斬斷?透過介面是一個最好的方式。所有與外界服務(包括類別),都應該相依於介面,而不是直接相依於物件。(也就是IoC的方式,IoC的範例可以參考:[如何提升系統品質-Day6]重構-簡單使用interface之『你也會IoC』)
這邊舉一個發票資料更新的例子 (圖片來源:Working Effectively with Legacy Code):
1.直接相依於DB class。
2.透過介面後,讓InvoiceUpdateResponder Class相依於IDBConnection,在production code中,再將DBConnection Class注入。
如此一來,在測試InvoiceUpdateResponder裡面的update()方法時,就不會與實際的DataBase相依。我們不再需要連接DataBase才能得到資料,而可以透過Stub Object的方式,直接定義該IDBConnection.getInvoices()回傳的資料清單。
何謂Stub
Stub指的是一種run time時建立的拋棄式instance,可以定義該Stub是繼承/實作哪一個介面或抽象類別,並指定哪一個方法該回傳什麼值(透過overrides方法),進而使得單元測試中的測試目標,不需與外界服務相依,而只需相依於介面,並將Stub Object注入。
實際的範例,請參考:[如何提升系統品質-Day7]測試-單元測試, Just Do It!!,裡面的步驟五,即透過Rhino.Mocks產生Stub:
1.定義這個stub物件是繼承/實作哪一個class(需要是abstract class或interface)
2.定義被呼叫哪一個方法
3.傳入哪一個參數
4.預計會回傳什麼值。
結論
透過整篇文章,您應該可以確定一下,自己寫的測試程式,是屬於整合測試,或是單元測試。
強烈建議,整合測試專案與單元測試專案要拆開來放,當在開發階段或在CI server上建立Auto Build的時候,每一次程式碼的改變(開發告一段落或簽入至版本庫),都需要執行一次完整的單元測試,才能確保這一次的版本是如同預期,且沒有影響到其他程式。
如果整合測試專案與單元測試專案綁在一起,那這個動作就會有上述整合測試的缺點:慢!
慢,就代表不好用。不好用就代表大家不想用。大家不想用就代表導入障礙提高。最後花的建置成本可能就會付諸流水。
如果您還沒開始使用單元測試,建議您跨出第一步,會感受到新奇、興奮以及相當的挫折感。挫折感的來源,來自於production code耦合性過高,可測試性低,代表品質不好。
程式不能測試=品質不好?
這麼說雖沒有錯,但不夠完整。
程式的可測試性高,代表程式的耦合度低,耦合度低,則代表程式品質『可能』有一定水準。但,程式的可測試性低,沒法子測試,或難以測試,難以『維護』測試,則代表程式耦合度高,或是程式內聚力低,則代表程式品質不佳。這是全然無誤的。
所以,程式的可測試性,是系統的重要品質指標之一。